/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.schemarepo.server;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.schemarepo.MessageStrings;
import org.schemarepo.Repository;
import org.schemarepo.SchemaEntry;
import org.schemarepo.SchemaValidationException;
import org.schemarepo.Subject;
import org.schemarepo.SubjectConfig;
import com.sun.jersey.api.NotFoundException;
/**
* {@link RESTRepository} Is a JSR-311 REST Interface to a {@link Repository}.
*
* Combine with {@link RepositoryServer} to run an embedded REST server.
*
* This is an abstract base class. Concrete implementations (such as
* {@link org.schemarepo.server.MachineOrientedRESTRepository} and {@link org.schemarepo.server.HumanOrientedRESTRepository})
* handle media types differently and are accessible via different paths, though the actual functionality of
* accessing the underlying repository server is contained in this class.
*/
public abstract class RESTRepository extends BaseRESTRepository {
/**
* Create a {@link RESTRepository} that wraps a given {@link Repository}
* Typically the wrapped repository is a
* {@link org.schemarepo.CacheRepository} that wraps a non-caching
* underlying repository.
*
* @param repo The {@link Repository} to wrap.
* @param renderers determine which content types (based on the <pre>Accept</pre> header) will be supported;
* the first renderer will act as default (handling missing or wildcard media type)
*/
public RESTRepository(Repository repo, List<? extends Renderer> renderers) {
super(repo, renderers);
}
/**
* No @Path annotation means this services the "/" endpoint.
*
* @return All subjects in the repository, serialized with {@link org.schemarepo.RepositoryUtil#subjectsToString(Iterable)}
*/
@GET
public Response allSubjects(@HeaderParam("Accept") String mediaType) {
Renderer renderer = getRenderer(mediaType);
return Response.ok(renderer.renderSubjects(repo.subjects()), renderer.getMediaType()).build();
}
/**
* Returns all schemas in the given subject, serialized with
* {@link org.schemarepo.RepositoryUtil#schemasToString(Iterable)}
*
* @param subject
* The name of the subject
* @return all schemas in the subject. Return a 404 Not Found if there is no such subject
*/
@GET
@Path("{subject}/all")
public Response allSchemaEntries(@HeaderParam("Accept") String mediaType, @PathParam("subject") String subject) {
Subject s = repo.lookup(subject);
if (null == s) {
throw new NotFoundException(MessageStrings.SUBJECT_DOES_NOT_EXIST_ERROR);
}
Renderer renderer = getRenderer(mediaType);
return Response.ok(renderer.renderSchemas(s.allEntries()), renderer.getMediaType()).build();
}
@GET
@Path("{subject}/config")
public String subjectConfig(@HeaderParam("Accept") String mediaType, @PathParam("subject") String subject) {
Subject s = repo.lookup(subject);
if (null == s) {
throw new NotFoundException(MessageStrings.SUBJECT_DOES_NOT_EXIST_ERROR);
}
Properties props = new Properties();
props.putAll(s.getConfig().asMap());
return getRenderer(mediaType).renderProperties(props, "Configuration of subject " + subject);
}
/**
* Create a subject if it does not already exist.
*
* @param subject
* the name of the subject
* @param configParams
* the configuration values for the Subject, as form parameters
* @return the subject name in a 200 response if successful.
* HTTP 404 if the subject does not exist, or HTTP 409 if there was a conflict creating the subject
*/
@PUT
@Path("{subject}")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response createSubject(@PathParam("subject") String subject, MultivaluedMap<String, String> configParams) {
if (null == subject) {
return Response.status(400).build();
}
SubjectConfig.Builder builder = new SubjectConfig.Builder();
for(Map.Entry<String, List<String>> entry : configParams.entrySet()) {
List<String> val = entry.getValue();
if(val.size() > 0) {
builder.set(entry.getKey(), val.get(0));
}
}
Subject created = repo.register(subject, builder.build());
return Response.ok(created.getName()).build();
}
/**
* Get the latest schema for a subject
*
* @param subject
* the name of the subject
* @return A 200 response with {@link SchemaEntry#toString()} as the body, or
* a 404 response if either the subject or latest schema is not found.
*/
@GET
@Path("{subject}/latest")
public String latest(@HeaderParam("Accept") String mediaType, @PathParam("subject") String subject) {
return getRenderer(mediaType).renderSchemaEntry(exists(getSubject(subject).latest()), true);
}
/**
* Look up a schema by subject + id pair.
*
* @param subject
* the name of the subject
* @param id
* the id of the schema
* @return A 200 response with the schema as the body, or a 404 response if
* the subject or schema is not found
*/
@GET
@Path("{subject}/id/{id}")
public String schemaFromId(@HeaderParam("Accept") String mediaType,
@PathParam("subject") String subject, @PathParam("id") String id)
{
return getRenderer(mediaType).renderSchemaEntry(exists(getSubject(subject).lookupById(id)), false);
}
/**
* Look up an id by a subject + schema pair.
*
* @param subject
* the name of the subject
* @param schema
* the schema to search for
* @return A 200 response with the id in the body, or a 404 response if the
* subject or schema is not found
*/
@POST
@Path("{subject}/schema")
@Consumes(MediaType.TEXT_PLAIN)
public String idFromSchema(@PathParam("subject") String subject, String schema) {
return exists(getSubject(subject).lookupBySchema(schema)).getId();
}
/**
* Register a schema with a subject
*
* @param subject
* The subject name to register the schema in
* @param schema
* The schema to register
* @return A 200 response with the corresponding id if successful,
* a 403 forbidden response with exception message if the schema fails validation,
* or a 404 not found response if the subject does not exist
*/
@PUT
@Path("{subject}/register")
@Consumes(MediaType.TEXT_PLAIN)
public Response addSchema(@PathParam("subject") String subject, String schema) {
try {
return Response.ok(getSubject(subject).register(schema).getId()).build();
} catch (SchemaValidationException e) {
return Response.status(Status.FORBIDDEN).entity(e.getMessage()).build();
}
}
/**
* Register a schema with a subject, only if the latest schema equals the
* expected value. This is for resolving race conditions between multiple
* registrations and schema invalidation events in underlying repositories.
*
* @param subject
* the name of the subject
* @param latestId
* the latest schema id, possibly null
* @param schema
* the schema to attempt to register
* @return a 200 response with the id of the newly registered schema, or a 404
* response if the subject or id does not exist or a 409 conflict if
* the id does not match the latest id or a 403 forbidden response
* with exception message if the schema fails validation
*/
@PUT
@Path("{subject}/register_if_latest/{latestId: .*}")
@Consumes(MediaType.TEXT_PLAIN)
public Response addSchema(@PathParam("subject") String subject,
@PathParam("latestId") String latestId, String schema) {
Subject s = getSubject(subject);
SchemaEntry latest;
if ("".equals(latestId)) {
latest = null;
} else {
latest = exists(s.lookupById(latestId));
}
SchemaEntry created;
try {
created = s.registerIfLatest(schema, latest);
if (null == created) {
return Response.status(Status.CONFLICT).build();
}
return Response.ok(created.getId()).build();
} catch (SchemaValidationException e) {
return Response.status(Status.FORBIDDEN).entity(e.getMessage()).build();
}
}
/**
* Get a subject
*
* @param subject
* the name of the subject
* @return a 200 response if the subject exists, or a 404 response if the
* subject does not.
*/
@GET
@Path("{subject}")
public Response checkSubject(@PathParam("subject") String subject) {
getSubject(subject);
return Response.ok().build();
}
@GET
@Path("{subject}/integral")
public String getSubjectIntegralKeys(@PathParam("subject") String subject) {
return Boolean.toString(getSubject(subject).integralKeys());
}
private Subject getSubject(String subjectName) {
Subject subject = repo.lookup(subjectName);
if (null == subject) {
throw new NotFoundException(MessageStrings.SUBJECT_DOES_NOT_EXIST_ERROR);
}
return subject;
}
private SchemaEntry exists(SchemaEntry entry) {
if (null == entry) {
throw new NotFoundException(MessageStrings.SCHEMA_DOES_NOT_EXIST_ERROR);
}
return entry;
}
}